package net.towee.server.bittorrent.impl;

import java.io.IOException;
import java.util.regex.Pattern;

import javax.inject.Inject;
import javax.inject.Named;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.towee.model.bittorrent.Peer;
import net.towee.model.bittorrent.TorrentData;
import net.towee.model.bittorrent.impl.PeerImpl;
import net.towee.server.CoreSettings;
import net.towee.server.bittorrent.AnnounceService;
import net.towee.server.persistence.AccountStore;
import net.towee.server.persistence.TorrentStore;
import net.towee.shared.exceptions.PersistenceException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Preconditions;
import com.mycila.event.internal.guava.base.Strings;

/**
 * Implementation of announce service
 * 
 * @author c58
 */
public class AnnounceServiceServlet extends HttpServlet implements AnnounceService {
	private static final long serialVersionUID = -315908022287624505L;
	private static final Logger LOG = LoggerFactory.getLogger(AnnounceServiceServlet.class);

	private final String IPV4_REGEX = "(([0-1]?[0-9]{1,2}\\.)|(2[0-4][0-9]\\.)|(25[0-5]\\.)){3}(([0-1]?[0-9]{1,2})|(2[0-4][0-9])|(25[0-5]))";
	private Pattern IPV4_PATTERN = Pattern.compile(IPV4_REGEX);

	private final TorrentStore torrStore;
	private final AccountStore accountStore;
	private final int announceInterval;

	@Inject
	public AnnounceServiceServlet(AccountStore accountStore, TorrentStore torrStore,
			@Named(CoreSettings.ANNOUNCE_INTERVAL) int interval) {
		this.accountStore = accountStore;
		this.torrStore = torrStore;
		this.announceInterval = interval;
	}

	@Override
	public void doGet(HttpServletRequest request, HttpServletResponse response) {
		Appendable out = null;

		try {
			// Get output stream writer
			out = response.getWriter();

			// Check required parameters
			Preconditions.checkArgument(!Strings.isNullOrEmpty(request.getParameter("info_hash")),
					AnnounceError.MISSING_INFOHASH);
			Preconditions.checkArgument(!Strings.isNullOrEmpty(request.getParameter("peer_id")),
					AnnounceError.MISSING_PEERID);
			Preconditions.checkArgument(!Strings.isNullOrEmpty(request.getParameter("port")),
					AnnounceError.MISSING_PORT);
			Preconditions.checkArgument(!Strings.isNullOrEmpty(request.getParameter("uploaded")),
					AnnounceError.MISSING_UL);
			Preconditions.checkArgument(!Strings.isNullOrEmpty(request.getParameter("downloaded")),
					AnnounceError.MISSING_DL);

			// Get account id
			String accountId = null;
			if(request.getRequestURI().length() > 10);
				accountId = request.getRequestURI().substring(10);

			// Get infoHash and peerId
			String infoHash = request.getParameter("info_hash");
			String peerId = request.getParameter("peer_id");

			// Get ip
			String ip = request.getParameter("ip");
			ip = (Strings.isNullOrEmpty(ip) || !isValidIPV4(ip)) ? request.getRemoteAddr() : ip;

			// Get down/upl
			int upload = Integer.parseInt(request.getParameter("uploaded"));
			int download = Integer.parseInt(request.getParameter("download"));

			// Get port
			int port = Integer.parseInt(request.getParameter("port"));
			Preconditions.checkArgument(port > 0, AnnounceError.MISSING_PORT);

			// Get event
			AnnounceEvent event = AnnounceEvent.REGULAR;
			if (request.getParameter("port").equals("started"))
				event = AnnounceEvent.STARTED;
			else if (request.getParameter("port").equals("stopped"))
				event = AnnounceEvent.STOPPED;
			else if (request.getParameter("port").equals("completed"))
				event = AnnounceEvent.COMPLETED;

			// Create and process announce request
			processAnnounceRequest(new AnnounceRequestImpl(accountId, infoHash, peerId, ip, port, upload,
					download, event), out);

		} catch (IllegalArgumentException e) {
			LOG.warn("Illegal argument exception while process announce request. Request was: "
					+ request.getRequestURL());

			writeError(AnnounceError.valueOf(e.getMessage()), out);
		} catch (IOException e) {
			LOG.error("Error while getting output stream writer!", e.getMessage());
		}
	}

	@Override
	public void processAnnounceRequest(AnnounceRequest request, Appendable out) {
		try {
			// Check account exists
			if( request.getAccountId() != null )
				accountStore.getAccountIdentity(request.getAccountId());

			// Get torrent data
			TorrentData torrent = torrStore.getTorrent(request.getInfoHash());

			// Process complete request
			if (request.getEvent().equals(AnnounceEvent.COMPLETED)) {
				// Decrement seeder if needed
				Peer peer = torrent.getPeer(request.getPeerId());
				if (peer != null && peer.isSeeder())
					torrent.decSeeders();

				// Remove peer
				torrent.removePeer(request.getPeerId());
			} else {
				// Create peer
				Peer peer = torrent.getPeer(request.getPeerId());
				if (peer == null) {
					peer = createPeer(request);
					torrent.addPeer(peer);
				}

				// Who is this peer, may be seeder?
				if (!peer.isSeeder() && request.getUploaded() > 0) {
					peer.setSeeder();
					torrent.incSeeders();
				}

				// Set last request date
				peer.setLastRequestDate();
			}

			// Update torrent
			torrStore.updateTorrent(torrent);

			// Write peers list
			writePeersAndInterval(torrent, out);
		} catch (PersistenceException e) {
			writeError(AnnounceError.UNKNOWN_TORRENT, out);
			LOG.warn("Error while getting account or torrent ", e.getMessage());
		}
	}

	@Override
	public void writePeersAndInterval(TorrentData torrent, Appendable out) {
		// Fast coder of output
		try {
			// Write header and interval
			out.append("d8:intervali"+announceInterval+"e");
			
			// Write peers
			out.append("5:peersl");
			for(Peer p : torrent.getPeersList()) {
				out.append("d");
				out.append("7:peer id"+p.getId().length()+":"+p.getId());
				out.append("2:ip"+p.getIp().length()+":"+p.getIp());
				out.append("4:porti"+p.getPort()+"e");
				out.append("e");
			}
			
			// Close peers list and header dictionary 
			out.append("ee");
		} catch (IOException e) {
			LOG.error("Error while sending announce response: ", e.getMessage());
		}
	}

	@Override
	public Peer createPeer(AnnounceRequest request) {
		return new PeerImpl(request.getAccountId(), request.getPeerId(), request.getIp(),
				request.getPort());
	}

	@Override
	public void writeError(AnnounceError error, Appendable out) {
		try {
			// Write key
			out.append("failure reason: ");

			// Write reason
			switch (error) {
			case MISSING_PORT:
				out.append("missing port");
				break;
			case MISSING_DL:
				out.append("missing downloaded number");
				break;
			case MISSING_UL:
				out.append("missing uploaded number");
				break;
			case MISSING_INFOHASH:
				out.append("missing info hash");
				break;
			case MISSING_PEERID:
				out.append("missing peer id");
				break;
			case INTERNAL_ERROR:
				out.append("internal error");
				break;
			case UNKNOWN_TORRENT:
				out.append("unknown torrent");
				break;
			}
		} catch (IOException e) {
			LOG.error("Error while sending failure message. ", e.getMessage());
		}
	}

	/**
	 * Validate ip address
	 * 
	 * @param s
	 * @return
	 */
	private boolean isValidIPV4(final String s) {
		return IPV4_PATTERN.matcher(s).matches();
	}
}
